/*
 * Copyright © 2017 eqxiu.com 北京中网易企秀科技有限公司  All rights reserved.
 */
package cn.hermit.core;

import java.io.FilterReader;
import java.io.IOException;
import java.io.Reader;

/**
 * A powerful reader which supports multiple byte or string reading way.<br/>
 * It is suitable to parse some meaningful Strings such like JSON, XML, CSV, YAML ...
 * 
 * <p>
 * The powerful reader can read bytes forward or backward in fix length or 
 * by delimiters or in quotes or by end of some string.
 * </p>
 * 
 * @author Jack Gao (Chinese name : GAO JIANGUO, Email : linux.gjg@gmail.com)
 * @date 17 Jun, 2014
 */
public class BackableReader extends FilterReader {

	/**
	 * A byte cache for forward and backward read
	 * 
	 */
	private char[] cache;

	/**
	 * Current position in cache
	 */
	private int readPos;

	/**
	 * Backward position in cache
	 */
	private int backPos;

	/**
	 * count for read bytes
	 */
	private int count;

	/**
	 * Indicate if the Reader is closed
	 */
	private boolean closed;

	/**
	 * Constructor with a java.io.Reader. Initialize read cache and backPos and readPos in cache.
	 * 
	 * @param in
	 * 		java.io.Reader
	 */
	public BackableReader(Reader in) {
		super(in);
		this.cache = new char[512];
		this.backPos = this.cache.length - 1;
		this.readPos = 0;
	}

	/**
	 * Constructor with a java.io.Reader and cache size. Initialize read cache and backPos and readPos in cache.
	 * 
	 * @param in
	 * 		java.io.Reader
	 * @param cacheSize
	 * 		the cache size
	 */
	public BackableReader(Reader in, int cacheSize) {
		super(in);
		if (cacheSize < 2) {
			cacheSize = 2;
		}
		this.cache = new char[cacheSize];
		this.backPos = this.cache.length - 1;
		this.readPos = 0;
	}

	/**
	 * Reads a single character. This method will block until a character is
	 * available, an I/O error occurs, or the end of the stream is reached.
	 * 
	 * <p>
	 * Subclasses that intend to support efficient single-character input should
	 * override this method.
	 * 
	 * @return The character read, as an integer in the range 0 to 65535 (
	 *         <tt>0x00-0xffff</tt>), or -1 if the end of the stream has been
	 *         reached
	 * 
	 * @exception IOException
	 *                If an I/O error occurs
	 */
	public int read() throws IOException {
		char cb[] = new char[1];
		if (read(cb, 0, 1) == -1)
			return -1;
		else
			return cb[0];
	}

	/**
	 * Read string until white space
	 * @return
	 * 		string until white space
	 * @throws IOException
	 */
	public String readTrim() throws IOException {
		StringBuilder r = new StringBuilder();
		int b = -1;
		while ((b = read()) == 0 || Character.isWhitespace(b)) {
			if (b == -1)
				break;
			r.append((char) b);
		}
		if (b != -1) {
			back();
		}
		return r.reverse().toString();
	}

	/**
	 * Read string in fix length
	 * 
	 * @param len
	 * 		The fix string length
	 * @return
	 * 		The string with fix length
	 * @throws IOException
	 */
	public String read(int len) throws IOException {
		char cb[] = new char[len];
		int length;
		if ((length = read(cb, 0, len)) == -1)
			return null;
		else
			return String.valueOf(cb, 0, length);
	}

	/**
	 * Read string in quotes
	 * 
	 * @param quote
	 * 		The quote until which to read string
	 * @return
	 * 		string in quotes
	 * @throws IOException
	 */
	public String readInQuotes(char quote) throws IOException {
		StringBuilder r = new StringBuilder();
		int b = -1;
		while (true) {
			b = read();
			if (b == -1) {
				return null;
			}
			r.append((char) b);
			if (b == '\\') {
				if ((b = read()) == -1) {
					return null;
				}
				r.append((char) b);
			} else if (b == quote) {
				break;
			}
		}
		if (r.length() == 0)
			return r.toString();
		if (r.charAt(r.length() - 1) == quote) {
			return r.substring(0, r.length() - 1);
		}
		return null;
	}

	/**
	 * Read string until delimiter
	 * 
	 * @param delim
	 * 		Delimiter until which to read
	 * @return
	 * 		string until delimiter
	 * @throws IOException
	 */
	public String read(char delim) throws IOException {
		StringBuilder r = new StringBuilder();
		int b = -1;
		while ((b = read()) != delim) {
			if (b == -1)
				break;
			r.append((char) b);
		}
		if (b != -1) {
			r.append((char) b);
		}
		return r.toString();
	}

	/**
	 * Read string until byte in delimiters
	 * 
	 * @param delims
	 * 		The delimiters
	 * @return
	 * 		String until byte in delimiters
	 * @throws IOException
	 */
	public String read(String delims) throws IOException {
		if (delims == null || "".equals(delims.trim())) {
			return null;
		}
		StringBuilder r = new StringBuilder();
		int b = -1;
		while (delims.indexOf(b = read()) == -1) {
			if (b == -1)
				break;
			r.append((char) b);
		}
		if (b != -1) {
			r.append((char) b);
		}
		return r.toString();
	}

	/**
	 * Read string end with some tail string
	 * @param end
	 * 		The tail string
	 * @return
	 * 		string end with some tail string
	 * @throws IOException
	 */
	public String readEndWith(String end) throws IOException {
		if (end == null || "".equals(end)) {
			return null;
		}
		StringBuilder r = new StringBuilder();
		int e = 0;
		int b = -1;
		while (true) {
			b = read();
			if (b == -1)
				return null;
			r.append((char) b);
			if (b == end.charAt(e++)) {
				if (e == end.length()) {
					break;
				}
			} else {
				e = 0;
			}
		}
		return r.toString();
	}

	/**
	 * The core method of Reader. Maintain a cache byte array in cycle by readPos and backPos.
	 * 
	 * @param cbuf
	 * 		Buffer of Reader
	 * @param off 
	 * 		The start position of read
	 * @param len
	 * 		The length to read
	 * 
	 */
	@Override
	public int read(char[] cbuf, int off, int len) throws IOException {
		if (closed) {
			return -1;
		}
		if (!ready() || (off + len) > cbuf.length) {
			closed = true;
			return -1;
		}
		if ((backPos + 1) % this.cache.length == readPos) {
			int length = super.read(cbuf, off, len);
			if (length == -1) {
				return length;
			}
			count += length;
			int copyStart = off;
			int copyLen = length;
			if (copyLen > this.cache.length - 1) {
				copyLen = this.cache.length - 1;
				copyStart = off + length - copyLen;
			}
			int delta = this.cache.length - readPos;
			if (copyLen > delta) {
				System.arraycopy(cbuf, copyStart, this.cache, readPos, delta);
				System.arraycopy(cbuf, delta, this.cache, 0, copyLen - delta);
			} else {
				System.arraycopy(cbuf, copyStart, this.cache, readPos, copyLen);
			}
			readPos = (readPos + copyLen) % this.cache.length;
			backPos = (readPos + this.cache.length - 1) % this.cache.length;
			return length;
		} else {
			int backStart = (backPos + 1) % this.cache.length;
			int cacheLen = readPos > backStart ? readPos - backStart : readPos
					+ this.cache.length - backStart;
			int delta = this.cache.length - backStart;
			if (len > cacheLen) {
				read(cbuf, 0, cacheLen);
				int length = read(cbuf, cacheLen, len - cacheLen);
				return cacheLen + length;
			} else {
				if (len > delta) {
					System.arraycopy(this.cache, backStart, cbuf, 0, delta);
					System.arraycopy(this.cache, 0, cbuf, delta, len - delta);
					backPos = (backPos + len) % this.cache.length;
				} else {
					System.arraycopy(this.cache, backStart, cbuf, 0, len);
					backPos = (backPos + len) % this.cache.length;
				}
				return len;
			}
		}
	}

	/**
	 * Read one byte backward
	 * 
	 * @return
	 * 		One byte backward
	 * @throws IOException
	 */
	public int back() throws IOException {
		char cb[] = new char[1];
		if (back(cb, 0, 1) == -1)
			return -1;
		else
			return cb[0];
	}

	/**
	 * Read string until white space in cache.
	 * 
	 * @return
	 * 		String until white space in cache
	 * @throws IOException
	 */
	public String backTrim() throws IOException {
		StringBuilder r = new StringBuilder();
		int b = -1;
		while ((b = back()) == 0 || Character.isWhitespace(b)) {
			r.append((char) b);
		}
		if (b != -1) {
			read();
		}
		return r.reverse().toString();
	}

	/**
	 * Read string with fix length in cache.
	 * 
	 * @param len
	 * 		The fix length of string to read
	 * @return
	 * 		String with fix length in cache.
	 * @throws IOException
	 */
	public String back(int len) throws IOException {
		char cb[] = new char[len];
		int length;
		if ((length = back(cb, 0, len)) == -1)
			return null;
		else
			return String.valueOf(cb, 0, length);
	}

	/**
	 * Read string in quotes in cache.
	 * 
	 * @param quote
	 * 		The quote in which to read string
	 * @return
	 * 		String in quotes in cache
	 * @throws IOException
	 */
	public String backInQuotes(char quote) throws IOException {
		StringBuilder r = new StringBuilder();
		int b = -1;
		while (true) {
			b = back();
			if (b == -1) {
				return null;
			}
			if (b == quote) {
				r.append((char) b);
				b = back();
				if (b == '\\') {
					r.append((char) b);
				} else {
					if (b != -1) {
						read();
					}
					break;
				}
			} else {
				r.append((char) b);
			}
		}
		if (r.length() == 0)
			return "";
		if (r.charAt(r.length() - 1) == quote) {
			return r.reverse().substring(1);
		}
		return null;
	}

	/**
	 * Read string backward until delimiter in cache
	 * 
	 * @param delim
	 * 		Delimiter until which to read
	 * @return
	 * 		string until delimiter in cache
	 * @throws IOException
	 */
	public String back(char delim) throws IOException {
		StringBuilder r = new StringBuilder();
		int b = -1;
		while ((b = back()) != delim) {
			r.append((char) b);
		}
		if (b != (char) -1) {
			r.append((char) b);
		}
		return r.reverse().toString();
	}

	/**
	 * Read string backward until byte in delimiters in cache.
	 * 
	 * @param delims
	 * 		The delimiters
	 * @return
	 * 		String until byte in delimiters in cache
	 * @throws IOException
	 */
	public String back(String delims) throws IOException {
		if (delims == null || "".equals(delims.trim())) {
			return null;
		}
		StringBuilder r = new StringBuilder();
		int b = -1;
		while (delims.indexOf(b = back()) == -1) {
			r.append((char) b);
		}
		if (b != -1) {
			r.append((char) b);
		}
		return r.reverse().toString();
	}

	/**
	 * Read string backward end with some tail string in cache.
	 * 
	 * @param end
	 * 		The tail string
	 * @return
	 * 		string end with some tail string
	 * @throws IOException
	 */
	public String backEndWith(String end) throws IOException {
		if (end == null || "".equals(end)) {
			return null;
		}
		StringBuilder r = new StringBuilder();
		int b = -1;
		int e = end.length() - 1;
		while (true) {
			b = back();
			if (b == -1) {
				return null;
			}
			r.append((char) b);
			if (b == end.charAt(e--)) {
				if (e == -1) {
					break;
				}
			} else {
				e = end.length() - 1;
			}
		}
		return r.reverse().toString();
	}

	/**
	 * The core method of Reader. Maintain a cache byte array in cycle by readPos and backPos.
	 * 
	 * @param cbuf
	 * 		Buffer of Reader
	 * @param off 
	 * 		The start position of read
	 * @param len
	 * 		The length to read
	 * 
	 */
	public int back(char[] cbuf, int off, int len) throws IOException {
		if (!canBack() || (off + len) > cbuf.length) {
			return -1;
		}
		int canBackLen = backPos > readPos ? (backPos - readPos) : (backPos + 1
				+ this.cache.length - readPos);
		int backLen = len;
		if (backLen > canBackLen) {
			backLen = canBackLen;
		}
		if (backLen > backPos + 1) {
			if (count < this.cache.length
					&& this.cache[this.cache.length - 1] == (char) 0) {
				backLen = backPos + 1;
				System.arraycopy(this.cache, 0, cbuf, 0, backLen);
				backPos = this.cache.length - 1;
			} else {
				System.arraycopy(this.cache, (this.cache.length - (backLen
						- backPos - 1)), cbuf, 0, (backLen - backPos - 1));
				System.arraycopy(this.cache, 0, cbuf, backLen - backPos - 1,
						backPos + 1);
				backPos = (backPos + this.cache.length - backLen)
						% this.cache.length;
			}
		} else {
			System.arraycopy(this.cache, backPos + 1 - backLen, cbuf, 0,
					backLen);
			backPos = (backPos + this.cache.length - backLen)
					% this.cache.length;
		}
		return backLen;
	}

	/**
	 * Check if can read backward.
	 * 
	 * @return
	 * @throws IOException
	 */
	public boolean canBack() throws IOException {
		return this.ready() && this.cache[backPos] != (char) 0
				&& backPos != readPos;
	}

	@Override
	public boolean ready() throws IOException {
		ensureOpen();
		return super.ready();
	}

	/** Checks to make sure that the stream has not been closed. */
	private void ensureOpen() throws IOException {
		if (this.cache == null) {
			closed = true;
			throw new IOException("Stream closed");
		}
	}

	@Override
	public void close() throws IOException {
		this.cache = null;
		closed = true;
		super.close();
	}

	/**
	 * Check if the Reader is closed
	 * @return
	 */
	public boolean isClosed() {
		return closed;
	}

	@Override
	public boolean markSupported() {
		return false;
	}

	@Override
	public void mark(int readAheadLimit) throws IOException {
		throw new IOException("mark/reset not supported");
	}

	@Override
	public void reset() throws IOException {
		throw new IOException("mark/reset not supported");
	}

	/**
	 * Get string in cache
	 * @return
	 * 		String in cache
	 */
	public String getCacheString() {
		if (this.cache == null)
			return "";
		if (count < this.cache.length
				&& this.cache[this.cache.length - 1] == (char) 0) {
			return String.valueOf(this.cache, 0, this.backPos + 1);
		} else {
			return String.valueOf(this.cache, this.readPos, this.cache.length
					- this.readPos)
					+ String.valueOf(this.cache, 0, this.backPos + 1);
		}
	}
}